package com.spzx.channel.service.impl;

import com.spzx.channel.service.LockService;
import com.spzx.common.core.utils.StringUtils;
import com.spzx.common.core.utils.uuid.UUID;
import com.spzx.common.redis.util.RedisLockUtil;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.Random;
import java.util.concurrent.TimeUnit;

@Service
public class LockServiceImpl implements LockService {
    @Resource
    StringRedisTemplate stringRedisTemplate;
    @Resource
    RedisLockUtil redisLockUtil;
    //8、redis分布式锁V7：
    // V6的问题：不支持重入性(获取到锁的业务方法 再次尝试获取锁)
    //         请求获取到锁以后 再次使用相同的key setnx设置失败 表示锁获取失败（会尝试自旋获取锁 代码阻塞 不能向后继续执行）
    // 解决： 同一个请求 获取到锁以后 再次获取同一个锁 应该可以获取成功
    /*
           修改获取锁的业务:
            1、如果锁空闲，直接setnx成功 表示获取到锁，次数默认设置为1
            2、如果锁被使用：
                2.1 被自己使用： 更新使用锁的次数+1
                2.2 被其它线程使用：获取锁失败

                分布式锁的信息：
                        锁的key：  保证多线程竞争 使用相同key的线程只有一个可以获取到锁
                        锁的值：    防止误删除，锁的标志 和线程绑定
                        锁的过期时间：防止死锁，业务异常 服务宕机释放锁的业务不能执行时 锁可以自然过期
                        锁的重入次数：实现重入性，获取到锁的线程可以多次获取同一把锁
                redis的String：  key val expire  无法满足上面的数据存储要求
                redis Hash结构可以存储复杂的数据：
                        hash结构： key 映射存储一个hash结构(hash由多对 property和value组成)
                        分部式锁的数据：
                               lockKey   uuid   count
                               expire lockKey timeout
                        获取锁时：
                            第一次：
                                lockKey  uuid1 1
                                expire lockKey 10
                            第二次：重入获取锁
                                判断锁的状态被使用，再判断锁是不是自己的，如果是自己的
                                lockKey uuid1 1+1

                        释放锁：
                            第一次：
                                判断锁的状态被使用，再判断锁是不是自己的，如果是自己的
                                lockKey uuid1 2-1
                            第二次：
                                判断锁的状态被使用，再判断锁是不是自己的，如果是自己的
                                lockKey uuid1 1-1
                                如果释放锁后 uuid1的值为0 表示锁完全释放
                                删除锁：del lockKey


          释放锁：
            1、释放锁时 使用锁的次数-1
            2、次数-1后的结果>0表示锁未完全释放：  重入使用同一个锁时 次数会+1
            3、次数-1后的结果=0 表示锁完全释放： 可以删除锁
     */
    @Override
    public void incr() {
        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间，并设置锁的唯一标志
        String uuid = UUID.randomUUID().toString().replace("-", "");
        String lockKey = "channel:lock:num";
        int timeout = 200;
        //通过工具类获取锁
        redisLockUtil.lock(lockKey, uuid, timeout);
        //===2、获取锁成功：执行业务
        //1、获取redis中缓存的num
        String numStr = stringRedisTemplate.opsForValue().get("num");
        if (StringUtils.isEmpty(numStr)) {
            //第一次：可以初始化num值
            numStr = "0";
        }
        //调用环境检查业务方法！！！
        check(lockKey,uuid,timeout);
        //2、执行num+1操作
        int num = Integer.parseInt(numStr);
        num++;
//            int i = 1/0;
        //3、将+1后的结果设置到redis中
        stringRedisTemplate.opsForValue().set("num", num + "");
        //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
        //通过工具类释放锁
        redisLockUtil.unlock(lockKey, uuid);
    }
    //检查环境安全方法
    public void check(String lockKey , String uuid , int timeout) {
        redisLockUtil.lock(lockKey,uuid,timeout);
        System.out.println("进行环境扫描检查");

        System.out.println("检查完毕......");
        redisLockUtil.unlock(lockKey,uuid);
    }

    //7、redis分布式锁V6： 抽取获取分布式锁和释放锁的工具类
//    @Override
//    public void incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间，并设置锁的唯一标志
//        String uuid = UUID.randomUUID().toString().replace("-", "");
//        String lockKey = "channel:lock:num";
//        int timeout = 10;
//        //通过工具类获取锁
//        redisLockUtil.lock(lockKey, uuid, timeout);
//        //===2、获取锁成功：执行业务
//        //1、获取redis中缓存的num
//        String numStr = stringRedisTemplate.opsForValue().get("num");
//        if (StringUtils.isEmpty(numStr)) {
//            //第一次：可以初始化num值
//            numStr = "0";
//        }
//        //2、执行num+1操作
//        int num = Integer.parseInt(numStr);
//        num++;
////            int i = 1/0;
//        //3、将+1后的结果设置到redis中
//        stringRedisTemplate.opsForValue().set("num", num + "");
//        //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
//        //通过工具类释放锁
//        redisLockUtil.unlock(lockKey, uuid);
//    }

    //6、redis分布式锁v5: 基于v4  解决锁过期失效问题
    // 解决： 可以通过当前服务中的一个子线程，判断如果业务未执行完成 锁即将过期时 给锁续期 更新过期时间
    //   watchdog线程：看门狗线程！- 如何判断获取到锁的业务未执行结束？ 线程获取到锁设置了自己的唯一标志uuid，当线程正常结束时会删除key和uuid，其它线程获取到锁时uuid一定不同
    //  业务： 获取到锁的线程，同时给它开启一个子线程, 子线程每过一段时间(锁过期时间的2/3)去判断一次 如果锁的值还是uuid将锁过期时间更新，如果不是uuid 子线程结束
    // 问题：不支持重入性！
//    @Override
//    public  void  incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间，并设置锁的唯一标志
//        String uuid = UUID.randomUUID().toString().replace("-","");
//        String lockKey = "channel:lock:num";
//        int timeout = 10;
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,
//                uuid, timeout, TimeUnit.SECONDS);
//        if(flag){
//            //===2、获取锁成功：执行业务
//            //开启看门狗线程 对锁自动续期
//            renewLock(lockKey , uuid , timeout);
//
//            //1、获取redis中缓存的num
//            String numStr = stringRedisTemplate.opsForValue().get("num");
//            if(StringUtils.isEmpty(numStr)){
//                //第一次：可以初始化num值
//                numStr = "0";
//            }
//            //2、执行num+1操作
//            int num = Integer.parseInt(numStr);
//            num++;
//            try {
//                Thread.sleep(800000);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
////            int i = 1/0;
//            //3、将+1后的结果设置到redis中
//            stringRedisTemplate.opsForValue().set("num" , num+"");
//            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
//            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
//            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
//            //       redis一次接收到的多个指令不会被其它的命令插队
////            if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
////                stringRedisTemplate.delete("channel:lock:num");
////            }
//            String releaseLockScipt = "local uuid = redis.call('get' , KEYS[1]) " +
//                    "  if( ARGV[1] == uuid) " +
//                    " then return redis.call('del' , KEYS[1]) " +
//                    " else return 0 end";
//            Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLockScipt, Long.class),
//                    Arrays.asList(lockKey), uuid);
//
//            System.out.println("释放锁的结果:"+result);
//
//        }else{
//            //===4、获取锁失败，再次尝试获取锁(递归调用)
//            //频繁调用可能会导致栈内存溢出： 线程保存未执行完的方法信息时，会通过栈结构(Stack)存储，默认空间为1m
//            try {
//                Thread.sleep(100+ new Random().nextInt(100));
//                incr();
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }
//
//    }
    //锁的自动续期业务
    private void renewLock(String lockKey, String uuid, int timeout) {
        //使用子线程
        new Thread(() -> {
            try {
                //1、第一次判断是否续期：让子线程休眠 timeout*2/3 秒再执行
                TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                //2、判断是否要续期
                // 到redis中获取lockKey的值 如果 和uuid相等 更新锁的过期时间
                // 更新成功返回1  其它情况返回0表示失败(watchdog线程可以结束了)
                String script = "local uuid = redis.call('get' , KEYS[1]) " +
                        " if uuid == ARGV[1] then return redis.call('expire' , KEYS[1] , ARGV[2]) " +
                        " else return 0 end";
                //如果执行更新过期时间的命令返回成功 休眠后 再次执行
                //如果执行更新过期时间的命令失败，结束循环   watchdog线程正常结束
                while (stringRedisTemplate.execute(new DefaultRedisScript<>(script, Boolean.class),
                        //class java.lang.Integer cannot be cast to class java.lang.String
                        Arrays.asList(lockKey), uuid, timeout + "")) { //execute接收的参数只能是字符串类型
                    TimeUnit.SECONDS.sleep(timeout * 2 / 3);
                }

                //如果不等，表示当前子线程 要监听的锁业务已经结束 已经删除了 看门狗线程可以结束了

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }

        }).start();

    }


    //5、redis分布式锁V4：基于V3 解决删除锁的原子性问题
    //  问题： 删除锁时，判断是不是自己的锁 和删除锁的逻辑 不具有原子性 不是线程安全的！
    //   解决： 需要通过LUA脚本 将redis的多个操作 和业务逻辑封装交给redis原子性执行
    //   问题： 业务时长如果超过了锁的过期时间，锁失效，其它线程可以获取到锁
//    @Override
//    public  void  incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间，并设置锁的唯一标志
//        String uuid = UUID.randomUUID().toString().replace("-","");
//        String lockKey = "channel:lock:num";
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey,
//                uuid,5, TimeUnit.SECONDS);
//        if(flag){
//            //===2、获取锁成功：执行业务
//            //1、获取redis中缓存的num
//            String numStr = stringRedisTemplate.opsForValue().get("num");
//            if(StringUtils.isEmpty(numStr)){
//                //第一次：可以初始化num值
//                numStr = "0";
//            }
//            //2、执行num+1操作
//            int num = Integer.parseInt(numStr);
//            num++;
////            try {
////                Thread.sleep(8000);
////            } catch (InterruptedException e) {
////                throw new RuntimeException(e);
////            }
////            int i = 1/0;
//            //3、将+1后的结果设置到redis中
//            stringRedisTemplate.opsForValue().set("num" , num+"");
//            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
//            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
//            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
//            //       redis一次接收到的多个指令不会被其它的命令插队
////            if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
////                stringRedisTemplate.delete("channel:lock:num");
////            }
//            String releaseLockScipt = "local uuid = redis.call('get' , KEYS[1]) " +
//                    "  if( ARGV[1] == uuid) " +
//                    " then return redis.call('del' , KEYS[1]) " +
//                    " else return 0 end";
//            Long result = stringRedisTemplate.execute(new DefaultRedisScript<>(releaseLockScipt, Long.class),
//                    Arrays.asList(lockKey), uuid);
//
//            System.out.println("释放锁的结果:"+result);
//
//        }else{
//            //===4、获取锁失败，再次尝试获取锁(递归调用)
//            //频繁调用可能会导致栈内存溢出： 线程保存未执行完的方法信息时，会通过栈结构(Stack)存储，默认空间为1m
//            try {
//                Thread.sleep(100+ new Random().nextInt(100));
//                incr();
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }
//
//    }
    //4、redis分布式锁V3：基于V2 解决误删除问题  给锁设置唯一的标志(uuid)
    //    解决：可以给每个线程自己的锁设置唯一的值，删除锁时判断 是自己的锁才删除，不是自己的不能删除
//    @Override
//    public  void  incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间，并设置锁的唯一标志
//        String uuid = UUID.randomUUID().toString().replace("-","");
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num",
//                uuid,5, TimeUnit.SECONDS);
//        if(flag){
//            //===2、获取锁成功：执行业务
//            //1、获取redis中缓存的num
//            String numStr = stringRedisTemplate.opsForValue().get("num");
//            if(StringUtils.isEmpty(numStr)){
//                //第一次：可以初始化num值
//                numStr = "0";
//            }
//            //2、执行num+1操作
//            int num = Integer.parseInt(numStr);
//            num++;
////            try {
////                Thread.sleep(8000);
////            } catch (InterruptedException e) {
////                throw new RuntimeException(e);
////            }
////            int i = 1/0;
//            //3、将+1后的结果设置到redis中
//            stringRedisTemplate.opsForValue().set("num" , num+"");
//            //===3、释放使用完的锁：先判断是不是自己的锁，如果是才可以删除
//            // 原子性问题： 判断锁是不是自己的 和删除锁的逻辑 没有原子性
//            //  解决: 需要保证 判断 和删除的逻辑具有原子性  可以利用redis的原子性 将一组redis操作和逻辑交给redis
//            //       redis一次接收到的多个指令不会被其它的命令插队
//            if(uuid.equals(stringRedisTemplate.opsForValue().get("channel:lock:num"))){
//                stringRedisTemplate.delete("channel:lock:num");
//            }
//        }else{
//            //===4、获取锁失败，再次尝试获取锁(递归调用)
//            //频繁调用可能会导致栈内存溢出： 线程保存未执行完的方法信息时，会通过栈结构(Stack)存储，默认空间为1m
//            try {
//                Thread.sleep(100+ new Random().nextInt(100));
//                incr();
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }
//
//    }

    //3、redis分布式锁V2：基于V1 可以给锁设置过期时间
    //  问题：误删除问题
    //       线程1获取到锁，执行业务，但是业务执行时长超过了 锁的过期时间
    //       线程2在线程1的锁过期时可以获取到锁，执行自己的业务
    //       线程1执行业务结束，需要释放锁：执行删除锁的操作
    //       线程3就可以获取到锁
    //       导致锁失效： 同时又两个线程执行
//    @Override
//    public  void  incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁  设置5秒的过期时间
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num",
//                "1",5, TimeUnit.SECONDS);
//        if(flag){
//            //===2、获取锁成功：执行业务
//            //1、获取redis中缓存的num
//            String numStr = stringRedisTemplate.opsForValue().get("num");
//            if(StringUtils.isEmpty(numStr)){
//                //第一次：可以初始化num值
//                numStr = "0";
//            }
//            //2、执行num+1操作
//            int num = Integer.parseInt(numStr);
//            num++;
////            int i = 1/0;
//            //3、将+1后的结果设置到redis中
//            stringRedisTemplate.opsForValue().set("num" , num+"");
//            //===3、释放使用完的锁：
//            stringRedisTemplate.delete("channel:lock:num");
//        }else{
//            //===4、获取锁失败，再次尝试获取锁(递归调用)
//            //频繁调用可能会导致栈内存溢出： 线程保存未执行完的方法信息时，会通过栈结构(Stack)存储，默认空间为1m
//            try {
//                Thread.sleep(100+ new Random().nextInt(100));
//                incr();
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }
//
//    }
    //2、redis分布式锁V1：通过redis的setnx尝试获取锁
    //      问题：业务出现异常、服务宕机导致锁未能正常释放 导致死锁
//    @Override
//    public  void  incr() {
//        //===1、进入方法时，通过redis的setnx尝试获取锁
//        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent("channel:lock:num", "1");
//        if(flag){
//            //===2、获取锁成功：执行业务
//            //1、获取redis中缓存的num
//            String numStr = stringRedisTemplate.opsForValue().get("num");
//            if(StringUtils.isEmpty(numStr)){
//                //第一次：可以初始化num值
//                numStr = "0";
//            }
//            //2、执行num+1操作
//            int num = Integer.parseInt(numStr);
//            num++;
//            int i = 1/0;
//            //3、将+1后的结果设置到redis中
//            stringRedisTemplate.opsForValue().set("num" , num+"");
//            //===3、释放使用完的锁：
//            stringRedisTemplate.delete("channel:lock:num");
//        }else{
//            //===4、获取锁失败，再次尝试获取锁(递归调用)
//            //频繁调用可能会导致栈内存溢出： 线程保存未执行完的方法信息时，会通过栈结构(Stack)存储，默认空间为1m
//            try {
//                Thread.sleep(100+ new Random().nextInt(100));
//                incr();
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
//
//        }
//
//    }
    //1、本地锁
//    @Override
//    public synchronized  void  incr() {
//        //1、获取redis中缓存的num
//        String numStr = stringRedisTemplate.opsForValue().get("num");
//        if(StringUtils.isEmpty(numStr)){
//            //第一次：可以初始化num值
//            numStr = "0";
//        }
//        //2、执行num+1操作
//        int num = Integer.parseInt(numStr);
//        num++;
//        //3、将+1后的结果设置到redis中
//        stringRedisTemplate.opsForValue().set("num" , num+"");
//    }
}
